| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | language: ruby | 
| 2 | +cache: bundler | |
| 2 | 3 | bundler_args: --without development production | 
| 3 | 4 | env: | 
| 4 | 5 | - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d TWITTER_OAUTH_KEY=twitteroauthkey TWITTER_OAUTH_SECRET=twitteroauthsecret | 
| @@ -0,0 +1,76 @@ | ||
| 1 | +module Agents | |
| 2 | + class ChangeDetectorAgent < Agent | |
| 3 | + cannot_be_scheduled! | |
| 4 | + | |
| 5 | + description <<-MD | |
| 6 | + The ChangeDetectorAgent receives a stream of events and emits a new event when a property of the received event changes. | |
| 7 | + | |
| 8 | + `property` specifies the property to be watched. | |
| 9 | + | |
| 10 | + `expected_update_period_in_days` is used to determine if the Agent is working. | |
| 11 | + | |
| 12 | + The resulting event will be a copy of the received event. | |
| 13 | + MD | |
| 14 | + | |
| 15 | + event_description <<-MD | |
| 16 | + This will change based on the source event. If you were event from the ShellCommandAgent, your outbound event might look like: | |
| 17 | + | |
| 18 | +      { | |
| 19 | + 'command' => 'pwd', | |
| 20 | + 'path' => '/home/Huginn', | |
| 21 | + 'exit_status' => '0', | |
| 22 | + 'errors' => '', | |
| 23 | + 'output' => '/home/Huginn' | |
| 24 | + } | |
| 25 | + MD | |
| 26 | + | |
| 27 | + def default_options | |
| 28 | +      { | |
| 29 | +          'property' => '{{output}}', | |
| 30 | + 'expected_update_period_in_days' => 1 | |
| 31 | + } | |
| 32 | + end | |
| 33 | + | |
| 34 | + def validate_options | |
| 35 | + unless options['property'].present? && options['expected_update_period_in_days'].present? | |
| 36 | + errors.add(:base, "The property and expected_update_period_in_days fields are all required.") | |
| 37 | + end | |
| 38 | + end | |
| 39 | + | |
| 40 | + def working? | |
| 41 | + event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? | |
| 42 | + end | |
| 43 | + | |
| 44 | + def receive(incoming_events) | |
| 45 | + incoming_events.each do |event| | |
| 46 | + handle(interpolated(event), event) | |
| 47 | + end | |
| 48 | + end | |
| 49 | + | |
| 50 | + private | |
| 51 | + | |
| 52 | + def handle(opts, event = nil) | |
| 53 | + property = opts['property'] | |
| 54 | + if has_changed?(property) | |
| 55 | + created_event = create_event :payload => event.payload | |
| 56 | + | |
| 57 | +        log("Propagating new event as property has changed to #{property} from #{last_property}", :outbound_event => created_event, :inbound_event => event ) | |
| 58 | + update_memory(property) | |
| 59 | + else | |
| 60 | +        log("Not propagating as incoming event has not changed from #{last_property}.", :inbound_event => event ) | |
| 61 | + end | |
| 62 | + end | |
| 63 | + | |
| 64 | + def has_changed?(property) | |
| 65 | + property != last_property | |
| 66 | + end | |
| 67 | + | |
| 68 | + def last_property | |
| 69 | + self.memory['last_property'] | |
| 70 | + end | |
| 71 | + | |
| 72 | + def update_memory(property) | |
| 73 | + self.memory['last_property'] = property | |
| 74 | + end | |
| 75 | + end | |
| 76 | +end | 
| @@ -1,27 +1,28 @@ | ||
| 1 | -<div class='container'> | |
| 2 | - <div class='row'> | |
| 3 | - <div class='span8 offset2'> | |
| 4 | - <div class='well'> | |
| 5 | - <h2>Forgot your password?</h2> | |
| 1 | +<div class='row'> | |
| 2 | + <div class='col-md-6 col-md-offset-3'> | |
| 3 | + <div class='well'> | |
| 4 | + <h2>Forgot your password?</h2> | |
| 6 | 5 |  | 
| 7 | -        <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> | |
| 8 | - <%= devise_error_messages! %> | |
| 6 | +      <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> | |
| 7 | + <%= devise_error_messages! %> | |
| 9 | 8 |  | 
| 10 | - <div class="control-group"> | |
| 11 | - <%= f.label :login, :class => 'control-label' %> | |
| 12 | - <div class="controls"> | |
| 13 | - <%= f.text_field :login, :class => 'span4' %> | |
| 14 | - </div> | |
| 9 | + <div class="form-group"> | |
| 10 | + <%= f.label :login, :class => 'col-md-2 col-md-offset-2 control-label' %> | |
| 11 | + <div class="col-md-6"> | |
| 12 | + <%= f.text_field :login, :class => 'form-control' %> | |
| 15 | 13 | </div> | 
| 14 | + </div> | |
| 16 | 15 |  | 
| 17 | - <div class='form-actions'> | |
| 16 | + <div class="form-group"> | |
| 17 | + <div class="col-md-offset-4 col-md-10"> | |
| 18 | 18 | <%= f.submit "Send me reset password instructions", :class => "btn btn-primary" %> | 
| 19 | 19 | </div> | 
| 20 | - <% end %> | |
| 20 | + </div> | |
| 21 | + <% end %> | |
| 21 | 22 |  | 
| 22 | - <%= render "devise/shared/links" %> | |
| 23 | + <hr> | |
| 23 | 24 |  | 
| 24 | - </div> | |
| 25 | + <%= render "devise/shared/links" %> | |
| 25 | 26 | </div> | 
| 26 | 27 | </div> | 
| 27 | 28 | </div> | 
| @@ -1,14 +1,28 @@ | ||
| 1 | -<h2>Resend unlock instructions</h2> | |
| 1 | +<div class='row'> | |
| 2 | + <div class='col-md-6 col-md-offset-3'> | |
| 3 | + <div class='well'> | |
| 4 | + <h2>Resend unlock instructions</h2> | |
| 2 | 5 |  | 
| 3 | -<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> | |
| 4 | - <%= devise_error_messages! %> | |
| 6 | +      <%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> | |
| 7 | + <%= devise_error_messages! %> | |
| 5 | 8 |  | 
| 6 | - <div> | |
| 7 | - <%= f.label :email %> | |
| 8 | - <%= f.email_field :email %> | |
| 9 | - </div> | |
| 9 | + <div class="form-group"> | |
| 10 | + <%= f.label :email, :class => 'col-md-2 col-md-offset-2 control-label' %> | |
| 11 | + <div class="col-md-6"> | |
| 12 | + <%= f.text_field :email, :class => 'form-control' %> | |
| 13 | + </div> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="form-group"> | |
| 17 | + <div class="col-md-offset-4 col-md-10"> | |
| 18 | + <%= f.submit "Resend unlock instructions", :class => "btn btn-primary" %> | |
| 19 | + </div> | |
| 20 | + </div> | |
| 21 | + <% end %> | |
| 10 | 22 |  | 
| 11 | - <div><%= f.submit "Resend unlock instructions" %></div> | |
| 12 | -<% end %> | |
| 23 | + <hr> | |
| 13 | 24 |  | 
| 14 | -<%= render "devise/shared/links" %> | |
| 25 | + <%= render "devise/shared/links" %> | |
| 26 | + </div> | |
| 27 | + </div> | |
| 28 | +</div> | 
| @@ -0,0 +1,98 @@ | ||
| 1 | +require 'spec_helper' | |
| 2 | + | |
| 3 | +describe Agents::ChangeDetectorAgent do | |
| 4 | + def create_event(output=nil) | |
| 5 | + event = Event.new | |
| 6 | + event.agent = agents(:jane_weather_agent) | |
| 7 | +    event.payload = { | |
| 8 | + :command => 'some-command', | |
| 9 | + :output => output | |
| 10 | + } | |
| 11 | + event.save! | |
| 12 | + | |
| 13 | + event | |
| 14 | + end | |
| 15 | + | |
| 16 | + before do | |
| 17 | +    @valid_params = { | |
| 18 | +        :property  => "{{output}}", | |
| 19 | + :expected_update_period_in_days => "1", | |
| 20 | + } | |
| 21 | + | |
| 22 | + @checker = Agents::ChangeDetectorAgent.new(:name => "somename", :options => @valid_params) | |
| 23 | + @checker.user = users(:jane) | |
| 24 | + @checker.save! | |
| 25 | + end | |
| 26 | + | |
| 27 | + describe "validation" do | |
| 28 | + before do | |
| 29 | + @checker.should be_valid | |
| 30 | + end | |
| 31 | + | |
| 32 | + it "should validate presence of property" do | |
| 33 | + @checker.options[:property] = nil | |
| 34 | + @checker.should_not be_valid | |
| 35 | + end | |
| 36 | + | |
| 37 | + it "should validate presence of property" do | |
| 38 | + @checker.options[:expected_update_period_in_days] = nil | |
| 39 | + @checker.should_not be_valid | |
| 40 | + end | |
| 41 | + end | |
| 42 | + | |
| 43 | + describe "#working?" do | |
| 44 | + before :each do | |
| 45 | + # Need to create an event otherwise event_created_within? returns nil | |
| 46 | + event = create_event | |
| 47 | + @checker.receive([event]) | |
| 48 | + end | |
| 49 | + | |
| 50 | + it "is when event created within :expected_update_period_in_days" do | |
| 51 | + @checker.options[:expected_update_period_in_days] = 2 | |
| 52 | + @checker.should be_working | |
| 53 | + end | |
| 54 | + | |
| 55 | + it "isnt when event created outside :expected_update_period_in_days" do | |
| 56 | + @checker.options[:expected_update_period_in_days] = 2 | |
| 57 | + | |
| 58 | + time_travel_to 2.days.from_now do | |
| 59 | + @checker.should_not be_working | |
| 60 | + end | |
| 61 | + end | |
| 62 | + end | |
| 63 | + | |
| 64 | + describe "#receive" do | |
| 65 | + before :each do | |
| 66 | +      @event = create_event("2014-07-01") | |
| 67 | + end | |
| 68 | + | |
| 69 | + it "creates events when memory is empty" do | |
| 70 | + @event.payload[:output] = "2014-07-01" | |
| 71 | +      expect { | |
| 72 | + @checker.receive([@event]) | |
| 73 | + }.to change(Event, :count).by(1) | |
| 74 | + Event.last.payload[:command].should == @event.payload[:command] | |
| 75 | + Event.last.payload[:output].should == @event.payload[:output] | |
| 76 | + end | |
| 77 | + | |
| 78 | + it "creates events when new event changed" do | |
| 79 | + @event.payload[:output] = "2014-07-01" | |
| 80 | + @checker.receive([@event]) | |
| 81 | + | |
| 82 | +      event = create_event("2014-08-01") | |
| 83 | + | |
| 84 | +      expect { | |
| 85 | + @checker.receive([event]) | |
| 86 | + }.to change(Event, :count).by(1) | |
| 87 | + end | |
| 88 | + | |
| 89 | + it "does not create event when no change" do | |
| 90 | + @event.payload[:output] = "2014-07-01" | |
| 91 | + @checker.receive([@event]) | |
| 92 | + | |
| 93 | +      expect { | |
| 94 | + @checker.receive([@event]) | |
| 95 | + }.to change(Event, :count).by(0) | |
| 96 | + end | |
| 97 | + end | |
| 98 | +end | 
| @@ -85,7 +85,7 @@ describe EventDrop do | ||
| 85 | 85 | before do | 
| 86 | 86 | @event = Event.new | 
| 87 | 87 | @event.agent = agents(:jane_weather_agent) | 
| 88 | - @event.created_at = Time.at(1400000000) | |
| 88 | + @event.created_at = Time.now | |
| 89 | 89 |      @event.payload = { | 
| 90 | 90 | 'title' => 'some title', | 
| 91 | 91 | 'url' => 'http://some.site.example.org/', | 
| @@ -115,6 +115,6 @@ describe EventDrop do | ||
| 115 | 115 |  | 
| 116 | 116 | it 'should have created_at' do | 
| 117 | 117 |      t = '{{created_at | date:"%FT%T%z" }}' | 
| 118 | -    interpolate(t, @event).should eq('2014-05-13T09:53:20-0700') | |
| 118 | +    interpolate(t, @event).should eq(@event.created_at.strftime("%FT%T%z")) | |
| 119 | 119 | end | 
| 120 | 120 | end | 
| @@ -9,7 +9,9 @@ shared_examples_for LiquidInterpolatable do | ||
| 9 | 9 |        "escape" => "This should be {{hello_world | uri_escape}}" | 
| 10 | 10 | } | 
| 11 | 11 |  | 
| 12 | - @checker = described_class.new(:name => "somename", :options => @valid_params) | |
| 12 | + @checker = new_instance | |
| 13 | + @checker.name = "somename" | |
| 14 | + @checker.options = @valid_params | |
| 13 | 15 | @checker.user = users(:jane) | 
| 14 | 16 |  | 
| 15 | 17 | @event = Event.new |